package com.pinfly.common.util;

import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * Maintain a HashMap using SoftReferences.
 * 
 * This class extends the standard HashMap class by wrapping the values in a
 * soft reference. This means that the values may be harvested by the garbage
 * collector if the system is running low on memory and they are not referenced
 * elsewhere. So items placed into the map may not be there when looked for
 * later. This is okay for a cache where the data can be recreated when needed,
 * but may not be suitable for other uses.
 * 
 * Note that even though a key may be in the set returned by the keySet method,
 * attempting to retrieve the value associated with the key may fail if the
 * garbage collector has cleared the soft reference.
 */
public class SoftHashMap extends HashMap
{
    /**
     * Associates the specified value with the specified key in this map.
     * 
     * The value is wrapped by a SoftReference before being placed in the map.
     * 
     * If the map previously contained a value for this key, the previous value
     * is returned.
     * 
     * @param key key with which the value is to be associated
     * @param value the value to associate with the key
     * 
     * @return the value previously associated with the key, if any. If no value
     *         was previously associated with the key, returns null
     */
    public Object put (Object key, Object value)
    {
        Object oldValue = super.put (key, new ComparableSoftReference (value));
        if (oldValue != null)
        {
            oldValue = ((SoftReference) oldValue).get ();
        }
        return oldValue;
    }

    /**
     * Return the value associated with a given key.
     * 
     * Looks up the value associated with the key in this map. Note that the
     * value will be a SoftReference. If the value is found, the object referred
     * to by the SoftReference (i.e. the value originally specified in the put
     * method) is returned. If there is no value, or the SoftReference has been
     * cleared (because the original object has been garbage collected) returns
     * null.
     * 
     * @param key key whose associated value is to be returned.
     * 
     * @return value associated with the key. If there is no value associated
     *         with the key, or the value object has been garbage collected,
     *         returns null.
     */
    public Object get (Object key)
    {
        Object value = super.get (key);
        if (value != null)
        {
            value = ((SoftReference) value).get ();
            if (value == null)
            {
                // The soft reference was cleared
                super.remove (key);
            }
        }
        return value;
    }

    /**
     * Returns true if the map contains the specified value.
     * 
     * @param value value whose presence is to be tested.
     * 
     * @return true if one or more keys map to the value; false if the value is
     *         not present in the map.
     */
    public boolean containsValue (Object value)
    {
        return super.containsValue (new ComparableSoftReference (value));
    }

    /**
     * Return a collection based view of the entries in the map.
     * <p>
     * Note that unlike the normal HashMap, the collection is not backed by the
     * map, so that updates to the map will not result in updates to the
     * collection. The collection represents a snapshot of the contents of the
     * map.
     */
    public Collection values ()
    {
        // Get the collection of values. These are the soft references.
        // Then build a collection of the referents for each soft reference
        // that is still valid.
        Collection v = super.values ();
        ArrayList l = new ArrayList (v.size ());
        for (Iterator it = v.iterator (); it.hasNext ();)
        {
            SoftReference ref = (SoftReference) it.next ();
            Object value = ref.get ();
            if (value != null)
                l.add (value);
        }
        return l;
    }

    /**
     * Returns a collection view of the mappings contained in this map. Each
     * element in the returned collection is a Map.Entry.
     * <p>
     * Not that unlike the normal HashMap, the collection is not backed by the
     * map, so that updates to the map will not result in updates to the
     * collection. The collection represents a snapshot of the contents of the
     * map.
     */
    public Set entrySet ()
    {
        // Get the set of all entries. The values in these entries
        // are the soft references. For each soft reference that is still
        // valid, replace the value in the entry with the referent and
        // add the entry to the collection to be returned.
        Set set = super.entrySet ();
        HashSet entries = new HashSet ();
        for (Iterator it = set.iterator (); it.hasNext ();)
        {
            Map.Entry e = (Map.Entry) it.next ();
            SoftReference ref = (SoftReference) e.getValue ();
            Object value = ref.get ();
            if (value != null)
            {
                e.setValue (value);
                entries.add (e);
            }
        }
        return entries;
    }

    /**
     * A comparable SoftReference class.
     * 
     * This class overrides the equals method for soft references to compare the
     * object referred to by the reference instead of comparing the reference
     * itself. So, given two soft references r1 (which refers to object o1) and
     * r2 (which refers to object o2), r1.equals(r2) will return true iff r1 has
     * not been cleared and o1.equals(o2).
     */
    private static class ComparableSoftReference extends SoftReference
    {
        private final int m_hashCode;

        public ComparableSoftReference (Object ref)
        {
            super (ref);
            // have to get the hashCode now in case the reference has
            // been cleared when some one asks for it
            m_hashCode = ref.hashCode ();
        }

        public boolean equals (Object other)
        {
            boolean ret = false;
            {
                if (other instanceof ComparableSoftReference)
                {
                    Object o1 = super.get ();
                    Object o2 = ((SoftReference) other).get ();
                    if (o1 != null && o1.equals (o2))
                    {
                        ret = true;
                    }
                }
            }
            return ret;
        }

        public int hashCode ()
        {
            return m_hashCode;
        }
    }

}
